package global;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import heap.Tuple;
import iterator.Iterator;

public class QueryCommand 
{
	public static Schema schema1;
    public static Schema schema2 = null;
	/*
	 * parses and runs query commands
	 */
	public static void query(String[] args)
	{
		if (args.length < 4) {
			System.out.println("Invalid format for query command.");
			System.out.println("Expected format is: query RELNAME1 (RELNAME2) QSNAME NUMBUF");
			return;
		}
		int tupleCnt = 0;

        String relName1;
        String relName2 = null;
        String qsname;
        String numbuf;

        if (args.length == 4) {
            relName1 = args[1];
            qsname = args[2];
            numbuf = args[3];
        } else {
            relName1 = args[1];
            relName2 = args[2];
            qsname = args[3];
            numbuf = args[4];
        }
		
		Iterator iterator = null;
		
		try {
			CommandLine.initializeDB(Integer.parseInt(numbuf));
			// get schema information for the relations
			schema1 = Schema.getSchema(relName1);
			if (schema1 == null) {
				throw new Exception("Schema 1 is not defined!");
			}

            if (relName2 != null) {
                schema2 = Schema.getSchema(relName2);
                if (schema2 == null) {
                    throw new Exception("Schema 2 is not defined!");
                }
            }
            // get the query command from the file
			Query query = parseQueryFromFile(qsname);
			iterator = query.getIterator(relName1, relName2, true);
			// loop through the results of the query and output the tuples
			Tuple tuple = iterator.get_next();
			while (tuple != null) {
				tupleCnt++;
				if (Commands.verbose)
				{
					outputProjectedTuple(tuple, relName1, query);
				}
				tuple = iterator.get_next();
			}

			// Print out information
			System.out.format("Tuples Returned: %d\n", tupleCnt);
		} catch (Exception e) {
			e.printStackTrace();
		}
		finally
		{
			try
			{
				// need to close the iterator to be sure it cleans up buffers and temp files
				if (iterator!=null)
				{
					iterator.close();
				}
			}
			catch (Exception e) {
				//do nothing
			}
		}
	}

    /*
     * Takes a tuple and formats the output for it
     */
	private static void outputProjectedTuple(Tuple tuple, String dbName, Query query) throws Exception {
		
		Vector100Dtype vector;
		double distance = -1;
		
		if(query.type == Query.queryType.NN)
		{
			vector = tuple.get100DVectFld(((NNQuery)query).fieldNumber);
			distance = Vector100Dtype.Distance(vector, ((NNQuery)query).target);
		} 
		
		else if(query.type == Query.queryType.Range) 
		{
			vector = tuple.get100DVectFld(((RangeQuery)query).fieldNumber);
			distance = Vector100Dtype.Distance(vector, ((RangeQuery)query).target);
		}
		
		else if(query.type == Query.queryType.Sort) 
		{
			vector = tuple.get100DVectFld(((SortQuery)query).FieldNumber);
			distance = Vector100Dtype.Distance(vector, ((SortQuery)query).Target);
		}

		if(distance >= 0) System.out.println("Distance: " + distance); 
		
		for(int i = 0; i < query.outFlds.length; i++)
		{
			int fldNum = query.outFlds[i];
			switch(query.outTypes[fldNum-1].attrType)
			{
			case AttrType.attrInteger:
			{
				int intAttr = tuple.getIntFld(fldNum);
				System.out.print(intAttr + "\t");
				
			}break;
			
			case AttrType.attrReal:
			{
				float floAttr = tuple.getFloFld(fldNum);
				System.out.print(floAttr + "\t");
			} break;
			
			case AttrType.attrString:
			{
				String strAttr = tuple.getStrFld(fldNum);
				System.out.print(strAttr + "\t");
			}break;
			
			case AttrType.attrVector100D:
			{
				Vector100Dtype v = tuple.get100DVectFld(fldNum);
				System.out.print(v.toString() + "\t");
			}break;
	
			}
		}
		System.out.println("\n");
	}
	
	// Takes a file as an input and parses the query from it
	
	private static Query parseQueryFromFile(String filename) throws Exception {
		return parseQueryFromText(getFileContents(filename));
	}
	
	// Takes a string and parses the query from it. Uses regular expressions to break it up into the different components
	
	private static Query parseQueryFromText(String text) throws Exception {
		Pattern pattern = Pattern.compile("^([^\\(]*)\\((.*)\\)");
		Matcher matcher = pattern.matcher(text);
		matcher.find();
		
		String queryName = matcher.group(1);
		String arguments = matcher.group(2);
		
		if (queryName.equals("Range")) {
			return parseRangeQuery(arguments);
		} else if (queryName.equals("NN")) {
			return parseNNQuery(arguments);
		} else if (queryName.equals("NLJ")) {
			return parseNLJQuery(arguments);
		} else if (queryName.equals("Sort")) {
			return parseSortQuery(arguments);
		} else if (queryName.equals("DJOIN")) {
			return parseDJOINquery(arguments);
		} else 
		{
			throw new Exception("Not a valid query");
		}
	}
	
	// Creates a NLJ Query from a string. Uses regular expressions to break it up into the different components
	
	private static DJOIN parseDJOINquery(String arguments) throws Exception {

	    
	   // Pattern pattern = Pattern.compile("^([^\\(]*)\\((.*)\\), ([^\\(]*)\\((.*)\\), (.*)");
	    Pattern pattern = Pattern.compile("^([^\\(]*)\\((.*)\\), (.*)");
		Matcher matcher = pattern.matcher(arguments);
		matcher.find();
		
		IndexType indexType = null;
		int QA2 = 0;
		int distance = 0;
		int[] innerFields = null;
		int noOfBits = 0;
		
		RangeQuery rangeQuery = null;
		NNQuery nnQuery = null;
		DJOINOuterQuery outerQuery = null;
		String function1 = matcher.group(1).trim();
		if (function1.equals("Range")) {//Range query!
			rangeQuery = parseRangeQuery(matcher.group(2));
			outerQuery = new DJOINOuterQuery(rangeQuery);
		} else if (function1.equals("NN")) {
			nnQuery = parseNNQuery(matcher.group(2));
			outerQuery = new DJOINOuterQuery(nnQuery);
		} else {
			throw new Exception("Invalid IndexNLJ function name");
		}
		
		String innerStr = matcher.group(3);
		pattern = Pattern.compile("^([0-9]*), ([0-9]*), ([NHB]{1}), (.*)");
		matcher = pattern.matcher(innerStr);
		boolean isMatch = matcher.find();
		
		QA2 = Integer.parseInt(matcher.group(1).trim());
		
		distance = Integer.parseInt(matcher.group(2).trim());
		
		String indexStr = matcher.group(3).trim();
		
		if(indexStr.equals("B")) {indexType = new IndexType(IndexType.VA_BT_Index);}
		else if(indexStr.equals("H")) {indexType = new IndexType(IndexType.VA_Index);}
		else {indexType = new IndexType(IndexType.None);}
		
		String additionalInputs = matcher.group(4).trim();
		if(indexType.indexType != IndexType.None)
		{
			pattern = Pattern.compile("^([0-9]*), (.*)");
			matcher = pattern.matcher(additionalInputs);
			isMatch = matcher.find();
			noOfBits = Integer.parseInt(matcher.group(1).trim()); //get number of bits
			additionalInputs = matcher.group(2).trim();
		}
		
		String[] outStrs = additionalInputs.split(" ");
		innerFields = new int[outStrs.length];
		
		for(int i = 0; i < innerFields.length; i++)
		{
			innerFields[i] = Integer.parseInt(outStrs[i]); //get the output fields of inner relation
		}
		
		
		DJOIN query = new DJOIN(outerQuery, indexType, QA2, innerFields, distance, noOfBits);
		query.type = Query.queryType.DJOIN1;
		return query;
	}
	
	// Creates a NLJ Query from a string. Uses regular expressions to break it up into the different components
	
	private static NLJQuery parseNLJQuery(String arguments) throws Exception {
		Pattern pattern = Pattern.compile("^([^\\(]*)\\((.*)\\), ([^\\(]*)\\((.*)\\), (.*)");
		Matcher matcher = pattern.matcher(arguments);
		matcher.find();
		
		RangeQuery rangeQuery = null;
		NNQuery nnQuery = null;
		String function1 = matcher.group(1).trim();
		if (function1.equals("Range")) {
			rangeQuery = parseRangeQuery(matcher.group(2));
		} else if (function1.equals("NN")) {
			nnQuery = parseNNQuery(matcher.group(2));
		} else {
			throw new Exception("Invalid NLJ function name");
		}
		
		String function2 = matcher.group(3).trim();
		if (function2.equals("Range")) {
			rangeQuery = parseRangeQuery(matcher.group(4));
		} else if (function2.equals("NN")) {
			nnQuery = parseNNQuery(matcher.group(4));
		} else {
			throw new Exception("Invalid NLJ function name");
		}
		
		int distance = Integer.parseInt(matcher.group(5));
		
		NLJQuery query = new NLJQuery(nnQuery, rangeQuery, distance);
		query.type = Query.queryType.NLJ;
		return query;
	}
	
	// Creates a Range Query from a string. Uses regular expressions to break it up into the different components
	
	private static RangeQuery parseRangeQuery(String arguments) throws Exception {
		try {
            Pattern pattern = Pattern.compile("^([0-9]*), ([^,]*), ([0-9]*), ([NHB]{1}), (.*)");
            Matcher matcher = pattern.matcher(arguments);
            boolean found = matcher.find();

            String str = matcher.group(1);
            int fieldNumber = Integer.parseInt(matcher.group(1).trim());
            String targetFile = matcher.group(2);
            int distance = Integer.parseInt(matcher.group(3).trim());
            String indexOption = matcher.group(4);
            String additionalOptions = matcher.group(5);
            int bits = -1;
            if (!indexOption.equals("N")) {
                // As long as an index option is used, the bits should also be passed in
                pattern = Pattern.compile("^([0-9]*), (.*)");
                matcher = pattern.matcher(additionalOptions);
                matcher.find();
                bits = Integer.parseInt(matcher.group(1));
                additionalOptions = matcher.group(2);
            }
            String[] outputFieldStrings = additionalOptions.split(" ");
            int[] outFlds = new int[outputFieldStrings.length];
            for (int i = 0; i < outFlds.length; i++) {
                outFlds[i] = Integer.parseInt(outputFieldStrings[i]);
            }

            String vectorFileContents = getFileContents(targetFile);
            Vector100Dtype target = new Vector100Dtype(vectorFileContents);

            RangeQuery query = new RangeQuery(fieldNumber, target, distance, outFlds, indexOption, bits);
            query.type = Query.queryType.Range;
            return query;
        } catch (Exception e) {
            System.out.println("Invalid format for range query command.");
            System.out.println("Expected format is: Range(QA, T, D, I, {B,} ...)");
            throw e;
        }
	}
	
	// Creates a NN Query from a string. Uses regular expressions to break it up into the different components
	
	private static NNQuery parseNNQuery(String arguments) throws Exception {
        try {
            Pattern pattern = Pattern.compile("^([0-9]*), ([^,]*), ([0-9]*), ([NHB]{1}), (.*)");
            Matcher matcher = pattern.matcher(arguments);
            matcher.find();
            
            String str = matcher.group(1);

            int fieldNumber = Integer.parseInt(matcher.group(1).trim());
            String targetFile = matcher.group(2);
            int k = Integer.parseInt(matcher.group(3));
            String indexOption = matcher.group(4);
            String additionalOptions = matcher.group(5);
            int bits = -1;
            if (!indexOption.equals("N")) {
                // As long as an index option is used, the bits should also be passed in
                pattern = Pattern.compile("^([0-9]*), (.*)");
                matcher = pattern.matcher(additionalOptions);
                matcher.find();
                bits = Integer.parseInt(matcher.group(1));
                additionalOptions = matcher.group(2);
            }
            String[] outputFieldStrings = additionalOptions.split(" ");
            int[] outFlds = new int[outputFieldStrings.length];
            for (int i = 0; i < outFlds.length; i++) {
                outFlds[i] = Integer.parseInt(outputFieldStrings[i]);
            }

            String vectorFileContents = getFileContents(targetFile);
            Vector100Dtype target = new Vector100Dtype(vectorFileContents);

            NNQuery query = new NNQuery(fieldNumber, target, k, outFlds, indexOption, bits);
            query.type = Query.queryType.NN;
            return query;
        } catch (Exception e) {
            System.out.println("Invalid format for NN query command.");
            System.out.println("Expected format is: NN(QA, T, K, I, {B,} ...)");
            throw e;
        }
	}

    // Creates a NN Query from a string. Uses regular expressions to break it up into the different components
	
	private static SortQuery parseSortQuery(String arguments) throws Exception {
        try {
            Pattern pattern = Pattern.compile("^([0-9]*), ([^,]*), (.*)");
            Matcher matcher = pattern.matcher(arguments);
            matcher.find();

            int fieldNumber = Integer.parseInt(matcher.group(1));
            String targetFile = matcher.group(2);
            String additionalOptions = matcher.group(3);
            String[] outputFieldStrings = additionalOptions.split(" ");
            int[] outFlds = new int[outputFieldStrings.length];
            for (int i = 0; i < outFlds.length; i++) {
                outFlds[i] = Integer.parseInt(outputFieldStrings[i]);
            }

            String vectorFileContents = getFileContents(targetFile);
            Vector100Dtype target = new Vector100Dtype(vectorFileContents);

            SortQuery query = new SortQuery(fieldNumber, target, outFlds);
            query.type = Query.queryType.Sort;
            return query;
        } catch (Exception e) {
            System.out.println("Invalid format for Sort query command.");
            System.out.println("Expected format is: Sort(QA, T, ...)");
            throw e;
        }
	}
	
	
	// Returns the contents of a file
	
	private static String getFileContents(String filename) throws IOException {
		BufferedReader br = new BufferedReader(new FileReader(filename));
	    StringBuffer text = new StringBuffer();
	    String line = br.readLine();
	    while (line != null) {
	    	text.append(line);
	    	text.append(" "); // Adding a space for each new line, essentially
	    	line = br.readLine();
	    }
	    br.close();
	    return text.toString();
	}

}
